/**
 * This module uses Mutation Observers to dynamically adjust which nodes will
 * generate Pointer Events.
 *
 * All nodes that wish to generate Pointer Events must have the attribute
 * `touch-action` set to `none`.
 */
import targeting from './targeting';

var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);
var map = Array.prototype.map.call.bind(Array.prototype.map);
var toArray = Array.prototype.slice.call.bind(Array.prototype.slice);
var filter = Array.prototype.filter.call.bind(Array.prototype.filter);
var MO = window.MutationObserver || window.WebKitMutationObserver;
var SELECTOR = '[touch-action]';
var OBSERVER_INIT = {
  subtree: true,
  childList: true,
  attributes: true,
  attributeOldValue: true,
  attributeFilter: ['touch-action']
};

function Installer(add, remove, changed, binder) {
  this.addCallback = add.bind(binder);
  this.removeCallback = remove.bind(binder);
  this.changedCallback = changed.bind(binder);
  if (MO) {
    this.observer = new MO(this.mutationWatcher.bind(this));
  }
}

Installer.prototype = {
  watchSubtree: function(target) {

    // Only watch scopes that can target find, as these are top-level.
    // Otherwise we can see duplicate additions and removals that add noise.
    //
    // TODO(dfreedman): For some instances with ShadowDOMPolyfill, we can see
    // a removal without an insertion when a node is redistributed among
    // shadows. Since it all ends up correct in the document, watching only
    // the document will yield the correct mutations to watch.
    if (this.observer && targeting.canTarget(target)) {
      this.observer.observe(target, OBSERVER_INIT);
    }
  },
  enableOnSubtree: function(target) {
    this.watchSubtree(target);
    if (target === document && document.readyState !== 'complete') {
      this.installOnLoad();
    } else {
      this.installNewSubtree(target);
    }
  },
  installNewSubtree: function(target) {
    forEach(this.findElements(target), this.addElement, this);
  },
  findElements: function(target) {
    if (target.querySelectorAll) {
      return target.querySelectorAll(SELECTOR);
    }
    return [];
  },
  removeElement: function(el) {
    this.removeCallback(el);
  },
  addElement: function(el) {
    this.addCallback(el);
  },
  elementChanged: function(el, oldValue) {
    this.changedCallback(el, oldValue);
  },
  concatLists: function(accum, list) {
    return accum.concat(toArray(list));
  },

  // register all touch-action = none nodes on document load
  installOnLoad: function() {
    document.addEventListener('readystatechange', function() {
      if (document.readyState === 'complete') {
        this.installNewSubtree(document);
      }
    }.bind(this));
  },
  isElement: function(n) {
    return n.nodeType === Node.ELEMENT_NODE;
  },
  flattenMutationTree: function(inNodes) {

    // find children with touch-action
    var tree = map(inNodes, this.findElements, this);

    // make sure the added nodes are accounted for
    tree.push(filter(inNodes, this.isElement));

    // flatten the list
    return tree.reduce(this.concatLists, []);
  },
  mutationWatcher: function(mutations) {
    mutations.forEach(this.mutationHandler, this);
  },
  mutationHandler: function(m) {
    if (m.type === 'childList') {
      var added = this.flattenMutationTree(m.addedNodes);
      added.forEach(this.addElement, this);
      var removed = this.flattenMutationTree(m.removedNodes);
      removed.forEach(this.removeElement, this);
    } else if (m.type === 'attributes') {
      this.elementChanged(m.target, m.oldValue);
    }
  }
};

export default Installer;
